﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace memcached
{
    /// <summary>
    /// Provide pooled cache store providers in a thread-safe manner.
    /// Thread safe, process-wide sync only.
    /// A pool is per the class instance only.
    /// </summary>
    public class CachePool : IDisposable
    {
        /// <summary>
        /// Instances available.
        /// </summary>
        protected readonly Enyim.Collections.InterlockedQueue<ICache> pool =
            new Enyim.Collections.InterlockedQueue<ICache>();
        /// <summary>
        /// Callback or null.
        /// </summary>
        protected readonly Func<ICache> creator;

        /// <summary>
        /// Number of created instances.
        /// </summary>
        protected long currentSize = 0;

        private readonly long maxSize;

        private readonly System.Threading.AutoResetEvent itemReleasedEvent =
            new System.Threading.AutoResetEvent(false);

        /// <summary>
        /// Create new pool on default memcached store.
        /// </summary>
        /// <param name="maxSize">positive</param>
        [System.Diagnostics.DebuggerHidden]
        public CachePool(int maxSize)
        {
            if (maxSize <= 0)
                throw new ArgumentOutOfRangeException("maxSize");
            this.maxSize = maxSize;
        }

        /// <summary>
        /// Create new pool using store provider.
        /// </summary>
        /// <param name="maxSize">positive</param>
        /// <param name="creator">not null</param>
        [System.Diagnostics.DebuggerHidden]
        public CachePool(int maxSize, Func<ICache> creator)
            : this(maxSize)
        {
            if (creator == null)
                throw new ArgumentNullException("creator");
            this.creator = creator;
        }

        /// <summary>
        /// Get cache from pool and perform operation on it.
        /// </summary>
        /// <param name="msTimeout">how long to wait for a cache available</param>
        /// <param name="operation">operation using cache from the pool</param>
        /// <returns>false if timed out waiting for cache</returns>
        public bool Perform(int msTimeout, Action<ICache> operation)
        {
            if (operation == null)
                throw new ArgumentNullException("operation");
            ICache c = Acquire(msTimeout);
            if (c == null)
                return false;
            try
            {
                operation(c);
                return true;
            }
            finally
            {
                Release(c);
            }
        }

        /// <summary>
        /// Get cache from pool and perform row of operations on it.
        /// </summary>
        /// <param name="msTimeout">how long to wait for a cache available</param>
        /// <param name="operations">operations using cache from the pool</param>
        /// <returns>false if timed out waiting for cache</returns>
        public bool Perform(int msTimeout, IEnumerable<Action<ICache>> operations)
        {
            if (operations == null)
                throw new ArgumentNullException("operations");
            ICache c = Acquire(msTimeout);
            if (c == null)
                return false;
            try
            {
                foreach (Action<ICache> operation in operations)
                {
                    if (operation == null)
                        throw new ArgumentNullException("operations", "Null item in parameter.");
                    operation(c);
                }
                return true;
            }
            finally
            {
                Release(c);
            }
        }

        /// <summary></summary>
        public bool Perform(int msTimeout, params Action<ICache>[] operations)
        {
            return Perform(msTimeout, operations.AsEnumerable());
        }

        /// <summary>
        /// Return cache store back to pool.
        /// </summary>
        /// <param name="c">not null</param>
        public void Release(ICache c)
        {
            if (c == null)
                throw new ArgumentNullException("c");
            pool.Enqueue(c);
            if (!itemReleasedEvent.SafeWaitHandle.IsClosed)
                itemReleasedEvent.Reset();
        }

        /// <summary>
        /// Take cache store from pool.
        /// </summary>
        /// <remarks>Will await for Release only when pool reached MaximumSize</remarks>
        /// <param name="msTimeout">wait timeout is milliseconds</param>
        /// <returns>null if timed out waiting or pool disposed</returns>
        public ICache Acquire(int msTimeout)
        {
            ICache client;
            if (pool.Dequeue(out client))
                return client;
            if (itemReleasedEvent.SafeWaitHandle.IsClosed)
                return null;// pool not available anymore
            long size = System.Threading.Interlocked.Read(ref currentSize);
            if (size >= maxSize)
            {
                if (msTimeout < 1)
                    throw new ArgumentOutOfRangeException("msTimeout");
                // no more available, need to wait for Release of an existing one
                if (itemReleasedEvent.WaitOne(msTimeout))
                    if (pool.Dequeue(out client))
                        return client;
                return null;// failed to acquire lock or dequeue on-time
            }
            System.Threading.Interlocked.Increment(ref currentSize);
            System.Diagnostics.Debug.Assert(currentSize > size);
            if (creator != null)
                client = creator();
            else
                client = new MemCache(true);
            return client;
        }

        /// <summary>
        /// Dispose all instances in the pool. The instance will not be usable anymore.
        /// </summary>
        public void Dispose()
        {
            GC.SuppressFinalize(this);
            ICache client;
            int disposed = 0;
            lock (pool)// only one thread can Dispose us, should be the pool owner
            {
                do
                {
                    while (pool.Dequeue(out client))
                    {
                        if (client is MemCache)
                            ((MemCache)client).DisposeInternal();
                        else
                            if (client is IDisposable)
                                ((IDisposable)client).Dispose();
                        disposed++;
                    }
                    // repeat until we dispose all our instances created
                } while (disposed < System.Threading.Interlocked.Read(ref currentSize));
                currentSize = 0;
                if (!itemReleasedEvent.SafeWaitHandle.IsClosed)
                    itemReleasedEvent.Close();
            }
        }

        /// <summary>
        /// Maximum number of items in the pool.
        /// </summary>
        public long MaximumSize
        {
            [System.Diagnostics.DebuggerHidden]
            get { return maxSize; }
        }

        /// <summary>
        /// Number of available items in the pool.
        /// </summary>
        public long CurrentSize
        {
            [System.Diagnostics.DebuggerHidden]
            get { return currentSize; }
        }
    }
}